/* eslint-disable no-unused-vars */
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { isTypedArray, isObject, isUndefinedOrNull } from './types';

export function deepClone<T>(obj: T): T {
  if (!obj || typeof obj !== 'object') {
    return obj;
  }
  if (obj instanceof RegExp) {
    return obj;
  }
  const result: any = Array.isArray(obj) ? [] : {};
  Object.entries(obj).forEach(([key, value]) => {
    result[key] = value && typeof value === 'object' ? deepClone(value) : value;
  });
  return result;
}

export function deepFreeze<T>(obj: T): T {
  if (!obj || typeof obj !== 'object') {
    return obj;
  }
  const stack: any[] = [obj];
  while (stack.length > 0) {
    const obj = stack.shift();
    Object.freeze(obj);
    for (const key in obj) {
      if (_hasOwnProperty.call(obj, key)) {
        const prop = obj[key];
        if (typeof prop === 'object' && !Object.isFrozen(prop) && !isTypedArray(prop)) {
          stack.push(prop);
        }
      }
    }
  }
  return obj;
}

const _hasOwnProperty = Object.prototype.hasOwnProperty;

export function cloneAndChange(obj: any, changer: (orig: any) => any): any {
  return _cloneAndChange(obj, changer, new Set());
}

function _cloneAndChange(obj: any, changer: (orig: any) => any, seen: Set<any>): any {
  if (isUndefinedOrNull(obj)) {
    return obj;
  }

  const changed = changer(obj);
  if (typeof changed !== 'undefined') {
    return changed;
  }

  if (Array.isArray(obj)) {
    const r1: any[] = [];
    for (const e of obj) {
      r1.push(_cloneAndChange(e, changer, seen));
    }
    return r1;
  }

  if (isObject(obj)) {
    if (seen.has(obj)) {
      throw new Error('Cannot clone recursive data-structure');
    }
    seen.add(obj);
    const r2 = {};
    for (const i2 in obj) {
      if (_hasOwnProperty.call(obj, i2)) {
        (r2 as any)[i2] = _cloneAndChange(obj[i2], changer, seen);
      }
    }
    seen.delete(obj);
    return r2;
  }

  return obj;
}

/**
 * Copies all properties of source into destination. The optional parameter "overwrite" allows to control
 * if existing properties on the destination should be overwritten or not. Defaults to true (overwrite).
 */
export function mixin(destination: any, source: any, overwrite: boolean = true): any {
  if (!isObject(destination)) {
    return source;
  }

  if (isObject(source)) {
    Object.keys(source).forEach((key) => {
      if (key in destination) {
        if (overwrite) {
          if (isObject(destination[key]) && isObject(source[key])) {
            mixin(destination[key], source[key], overwrite);
          } else {
            destination[key] = source[key];
          }
        }else {
          destination[key] = source[key];
        }
      } 
    });
  }
  return destination;
}

export function equals(one: any, other: any): boolean {
  if (one === other) {
    return true;
  }
  if (one === null || one === undefined || other === null || other === undefined) {
    return false;
  }
  if (typeof one !== typeof other) {
    return false;
  }
  if (typeof one !== 'object') {
    return false;
  }
  if (Array.isArray(one) !== Array.isArray(other)) {
    return false;
  }

  let i: number;
  let key: string;

  if (Array.isArray(one)) {
    if (one.length !== other.length) {
      return false;
    }
    for (i = 0; i < one.length; i++) {
      if (!equals(one[i], other[i])) {
        return false;
      }
    }
  } else {
    const oneKeys: string[] = [];

    for (key in one) {
      oneKeys.push(key);
    }
    oneKeys.sort();
    const otherKeys: string[] = [];
    for (key in other) {
      otherKeys.push(key);
    }
    otherKeys.sort();
    if (!equals(oneKeys, otherKeys)) {
      return false;
    }
    for (i = 0; i < oneKeys.length; i++) {
      if (!equals(one[oneKeys[i]], other[oneKeys[i]])) {
        return false;
      }
    }
  }
  return true;
}

/**
 * Calls `JSON.Stringify` with a replacer to break apart any circular references.
 * This prevents `JSON`.stringify` from throwing the exception
 *  "Uncaught TypeError: Converting circular structure to JSON"
 */
export function safeStringify(obj: any): string {
  const seen = new Set<any>();
  return JSON.stringify(obj, (key, value) => {
    if (isObject(value) || Array.isArray(value)) {
      if (seen.has(value)) {
        return '[Circular]';
      } else {
        seen.add(value);
      }
    }
    return value;
  });
}

type obj = { [key: string]: any };
/**
 * Returns an object that has keys for each value that is different in the base object. Keys
 * that do not exist in the target but in the base object are not considered.
 *
 * Note: This is not a deep-diffing method, so the values are strictly taken into the resulting
 * object if they differ.
 *
 * @param base the object to diff against
 * @param obj the object to use for diffing
 */
export function distinct(base: obj, target: obj): obj {
  const result = Object.create(null);

  if (!base || !target) {
    return result;
  }

  const targetKeys = Object.keys(target);
  targetKeys.forEach((k) => {
    const baseValue = base[k];
    const targetValue = target[k];

    if (!equals(baseValue, targetValue)) {
      result[k] = targetValue;
    }
  });

  return result;
}

export function getCaseInsensitive(target: obj, key: string): any {
  const lowercaseKey = key.toLowerCase();
  const equivalentKey = Object.keys(target).find((k) => k.toLowerCase() === lowercaseKey);
  return equivalentKey ? target[equivalentKey] : target[key];
}

export function filter(obj: obj, predicate: (key: string, value: any) => boolean): obj {
  const result = Object.create(null);
  for (const [key, value] of Object.entries(obj)) {
    if (predicate(key, value)) {
      result[key] = value;
    }
  }
  return result;
}

export function getAllPropertyNames(obj: object): string[] {
  let res: string[] = [];
  let proto = Object.getPrototypeOf(obj);
  while (Object.prototype !== proto) {
    res = res.concat(Object.getOwnPropertyNames(proto));
    proto = Object.getPrototypeOf(proto);
  }
  return res;
}

export function getAllMethodNames(obj: object): string[] {
  const methods: string[] = [];
  for (const prop of getAllPropertyNames(obj)) {
    if (typeof (obj as any)[prop] === 'function') {
      methods.push(prop);
    }
  }
  return methods;
}

export function createProxyObject<T extends object>(
  methodNames: string[],
  invoke: (method: string, args: unknown[]) => unknown,
): T {
  const createProxyMethod = (method: string): (() => unknown) => {
    return function () {
      const args = Array.prototype.slice.call(arguments, 0);
      return invoke(method, args);
    };
  };

  const result = {} as T;
  for (const methodName of methodNames) {
    (<any>result)[methodName] = createProxyMethod(methodName);
  }
  return result;
}
